{- This file is part of funbot.
 -
 - Written in 2015, 2016 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

-- For the 'MonadSettings' instance
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}

-- For JSON field names
{-# LANGUAGE OverloadedStrings #-}

module FunBot.Settings.Instances () where

import Control.Applicative
import Data.Aeson
import Data.Aeson.Types (typeMismatch)
import Data.Bool (bool)
import Data.CaseInsensitive (CI)
import Data.Hashable (Hashable)
import Data.Maybe (catMaybes)
import Data.Monoid ((<>))
import Data.Settings.Types
import FunBot.Types
import Network.IRC.Fun.Bot.State
import Network.IRC.Fun.Types.Base (Nickname (..))

import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S
import qualified Data.Text as T

instance MonadSettings BotSession Settings where
    getSettings      = getStateS bsSettings

    putSettings s    = modifyState $ \ st -> st { bsSettings = s }

    modifySettings f =
        modifyState $ \ st -> st { bsSettings = f $ bsSettings st }

    getSTree         = getStateS bsSTree

instance OptionValue Bool where
    readOption s
        | s' `elem` ["off", "false", "no",  "n", "0", "[_]"] = Just False
        | s' `elem` ["on",  "true",  "yes", "y", "1", "[x]"] = Just True
        | otherwise                                          = Nothing
        where
        s' = T.toLower s
    showOption = bool "False" "True"
    typeName = const "Boolean"

instance OptionValue T.Text where
    readOption = Just
    showOption = id
    typeName = const "String"

instance OptionValue [T.Text] where
    readOption = mapM readOption . T.split (== ',')
    showOption = T.intercalate "," . map showOption
    typeName   = const "List"

instance OptionValue (Maybe LinkDetectionMode) where
    readOption s =
        case s of
            "none"       -> Just Nothing
            "message"    -> Just (Just LDMMessage)
            "first-word" -> Just (Just LDMFirstWord)
            "all-words"  -> Just (Just LDMAllWords)
            _            -> Nothing
    showOption Nothing = "none"
    showOption (Just LDMMessage) = "message"
    showOption (Just LDMFirstWord) = "first-word"
    showOption (Just LDMAllWords) = "all-words"
    typeName = const "URL Detection Mode"

instance FromJSON LinkDetectionMode where
    parseJSON (String t) =
        case t of
            "message"    -> return LDMMessage
            "first-word" -> return LDMFirstWord
            "all-words"  -> return LDMAllWords
            _            -> fail "Unrecognized LinkDetectionMode value"
    parseJSON v          = typeMismatch "LinkDetectionMode" v

instance ToJSON LinkDetectionMode where
    toJSON LDMMessage   = String "message"
    toJSON LDMFirstWord = String "first-word"
    toJSON LDMAllWords  = String "all-words"

instance FromJSON a => FromJSON (Filter a) where
    parseJSON (Object o) =
        Accept <$> o .: "accept" <|>
        Reject <$> o .: "reject"
    parseJSON v          = typeMismatch "Filter" v

instance ToJSON a => ToJSON (Filter a) where
    toJSON (Accept l) = object [ "accept" .= l ]
    toJSON (Reject l) = object [ "reject" .= l ]

instance FromJSON RepoAnnSpec where
    parseJSON (Object o) =
        RepoAnnSpec <$>
        o .: "channel" <*>
        o .: "branches" <*>
        o .: "all-commits" <*>
        o .: "commits" <*>
        o .: "issues" <*>
        o .: "merge-requests" <*>
        o .: "snippets" <*>
        o .: "notes" <*>
        o .: "new" <*>
        o .: "old" <*>
        o .: "untimed"
    parseJSON v          = typeMismatch "RepoAnnSpec" v

instance ToJSON RepoAnnSpec where
    toJSON ras = object
        [ "channel"        .= rasChannel ras
        , "branches"       .= rasBranches ras
        , "all-commits"    .= rasAllCommits ras
        , "commits"        .= rasCommits ras
        , "issues"         .= rasIssues ras
        , "merge-requests" .= rasMergeRequests ras
        , "snippets"       .= rasSnippets ras
        , "notes"          .= rasNotes ras
        , "new"            .= rasNew ras
        , "old"            .= rasOld ras
        , "untimed"        .= rasUntimed ras
        ]

instance FromJSON NewsItemFields where
    parseJSON (Object o) =
        NewsItemFields <$>
        o .: "show-feed-title" <*>
        o .: "show-author" <*>
        o .: "show-url"
    parseJSON v          = typeMismatch "NewsItemFields" v

instance ToJSON NewsItemFields where
    toJSON (NewsItemFields ftitle author url) = object
        [ "show-feed-title" .= ftitle
        , "show-author"     .= author
        , "show-url"        .= url
        ]

instance FromJSON NewsAnnSpec where
    parseJSON (Object o) =
        NewsAnnSpec <$>
        o .: "channels" <*>
        o .: "fields"
    parseJSON v          = typeMismatch "NewsAnnSpec" v

instance ToJSON NewsAnnSpec where
    toJSON (NewsAnnSpec channels fields) = object
        [ "channels" .= channels
        , "fields"   .= fields
        ]

instance FromJSON NewsFeed where
    parseJSON (Object o) =
        NewsFeed <$>
        o .: "url" <*>
        o .: "active" <*>
        o .: "ann-spec"
    parseJSON v          = typeMismatch "NewsFeed" v

instance ToJSON NewsFeed where
    toJSON (NewsFeed url active spec) = object
        [ "url"      .= url
        , "active"   .= active
        , "ann-spec" .= spec
        ]

instance FromJSON a => FromJSON (M.HashMap (CI T.Text) a) where
    parseJSON v =
        let f (t, x) = (CI.mk t, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap (CI T.Text) a) where
    toJSON m =
        let f (t, x) = (CI.original t, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap (RepoSpace, RepoName) a) where
    parseJSON v =
        let mkpair (t, x) =
                case T.split (== '/') t of
                    [space, repo] ->
                        if T.null space || T.null repo
                            then Nothing
                            else Just
                                    ( ( RepoSpace $ CI.mk space
                                      , RepoName $ CI.mk repo
                                      )
                                    , x
                                    )
                    _ -> Nothing
        in  M.fromList . catMaybes . map mkpair . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap (RepoSpace, RepoName) a) where
    toJSON m =
        let unpair ((RepoSpace s, RepoName r), x) = (s <> "/" <> r, x)
        in  toJSON $ M.fromList $ map unpair $ M.toList m

instance FromJSON a => FromJSON (M.HashMap DevHostLabel a) where
    parseJSON v =
        let f (h, x) = (DevHostLabel h, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap DevHostLabel a) where
    toJSON m =
        let f (DevHostLabel h, x) = (h, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap DevHost a) where
    parseJSON v =
        let f (h, x) = (DevHost h, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap DevHost a) where
    toJSON m =
        let f (DevHost h, x) = (h, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap Nickname a) where
    parseJSON v =
        let f (n, x) = (Nickname n, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap Nickname a) where
    toJSON m =
        let f (Nickname n, x) = (n, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap FeedLabel a) where
    parseJSON v =
        let f (l, x) = (FeedLabel l, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap FeedLabel a) where
    toJSON m =
        let f (FeedLabel l, x) = (l, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap ShortcutLabel a) where
    parseJSON v =
        let f (l, x) = (MkShortcutLabel l, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap ShortcutLabel a) where
    toJSON m =
        let f (MkShortcutLabel l, x) = (l, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON a => FromJSON (M.HashMap LocationLabel a) where
    parseJSON v =
        let f (l, x) = (LocationLabel l, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap LocationLabel a) where
    toJSON m =
        let f (LocationLabel l, x) = (l, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON ShortcutDeprecated where
    parseJSON (Object o) =
        ShortcutDeprecated <$>
        o .: "prefix" <*>
        o .: "before" <*>
        o .: "after" <*>
        o .: "channels"
    parseJSON v          = typeMismatch "ShortcutDeprecated" v

instance FromJSON ChanSettings where
    parseJSON (Object o) =
        ChanSettings
        <$> (Just <$> o .: "say-titles" <|> return Nothing)
        <*> o .: "welcome"
        <*> o .: "email"
        <*> (S.map Nickname <$> o .: "puppeteers")
        <*> o .: "browse"
    parseJSON v          = typeMismatch "ChanSettings" v

instance ToJSON ChanSettings where
    toJSON (ChanSettings sayTitles welcome email pts url) = object
        [ "say-titles" .= sayTitles
        , "welcome"    .= welcome
        , "email"      .= email
        , "puppeteers" .= S.map unNickname pts
        , "browse"     .= url
        ]

instance FromJSON Settings where
    parseJSON (Object o) =
        Settings <$>
        o .: "repos"                              <*>
        o .: "feeds"                              <*>
        o .:? "shortcuts" .!= M.empty             <*>
        o .: "channels"                           <*>
        (M.map DevHostLabel <$> o .: "dev-hosts") <*>
        (S.map Nickname <$> o .: "puppeteers")    <*>
        (S.map Nickname <$> o .: "angels")        <*>
        (S.map Nickname <$> o .: "demons")
    parseJSON v          = typeMismatch "Settings" v

instance ToJSON Settings where
    toJSON (Settings repos feeds shortcuts chans hosts pts as ds) = object
        [ "repos"      .= repos
        , "feeds"      .= feeds
        , "channels"   .= chans
        , "dev-hosts"  .= M.map unDevHostLabel hosts
        , "puppeteers" .= S.map unNickname pts
        , "angels"     .= S.map unNickname as
        , "demons"     .= S.map unNickname ds
        ]
